﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Mime;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using JasLib.Interop.Win32;

namespace JasLib.Extensions
{
    public static class GeneralExtensions
    {
        /// <summary>
        /// This caches results returned from GetAllTypes().
        /// </summary>
        private static readonly Dictionary<Type, HashSet<Type>> s_derivedTypesDictionary = new Dictionary<Type, HashSet<Type>>();

        /**********************************************************************************/
        /// <summary>
        /// Tests whether the value is within the range, exclusive of the bounds.
        /// Not nearly as efficient as a pure operator comparison, but vastly more readable.
        /// </summary>
        public static bool Between<T>(this T me, T lower, T upper) where T : IComparable<T>
        {
            return (me.CompareTo(lower) > 0) && (me.CompareTo(upper) < 0);
        }

        /**********************************************************************************/
        public static bool BetweenEnums(this Enum value, Enum lower, Enum upper)
        {
            Debug.Assert(lower.CompareTo(upper) <= 0, "BetweenEnums() failed: lower limit was greater than upper limit.");
            return (value.CompareTo(lower) >= 0) && (value.CompareTo(upper) <= 0);
        }

        /**********************************************************************************/
        /// <summary>
        /// Comparer that treats a null Nullable&lt;T&gt; as less than a non-null Nullable&lt;T&gt;,
        /// thus avoiding awkward null checks or exceptions.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="item1"></param>
        /// <param name="item2"></param>
        /// <returns></returns>
        public static int CompareTo<T>(this Nullable<T> item1, Nullable<T> item2) where T : struct, IComparable<T>
        {
            if (!item1.HasValue && item2.HasValue)
                return -1;
            else if (!item1.HasValue && !item2.HasValue)
                return 0;
            else if (item1.HasValue && !item2.HasValue)
                return 1;
            else
                return item1.Value.CompareTo(item2.Value);
        }

        /**********************************************************************************/
        public static T Constrain<T>(this T value, T min, T max) where T : IComparable<T>
        {
            if (value.CompareTo(min) < 0)
                return min;
            else if (value.CompareTo(max) > 0)
                return max;
            else
                return value;
        }

        /**********************************************************************************/
        /// <summary>
        /// Removes the need for a typecast on ICloneable.Clone().
        /// The one (rare) danger is that the Clone() implementation might be a base implementation
        /// and not an override, in which case the cast to T will fail.
        /// But this would be more a case of implementer negligence than framework oversight.
        /// </summary>
        public static T DeepClone<T>(this T t)
            where T : ICloneable
        {
            return (T)((ICloneable)t).Clone();
        }

        /**********************************************************************************/
        public static FieldInfo[] GetAllFields(this Type type)
        {
            if (type == null)
                return null;
            return type.GetFields((BindingFlags)0x7FFFFFFF);
        }

        /**********************************************************************************/
        public static MethodInfo[] GetAllMethods(this Type type)
        {
            if (type == null)
                return null;
            return type.GetMethods((BindingFlags)0x7FFFFFFF);
        }

        /**********************************************************************************/
        public static PropertyInfo[] GetAllProperties(this Type type)
        {
            if (type == null)
                return null;
            return type.GetProperties((BindingFlags)0x7FFFFFFF);
        }

        /**********************************************************************************/
        /// <summary>
        /// Retrieves every single type in the inheritance hierarchy,
        /// including itself, interfaces, and base generic and enum types.
        /// </summary>
        private static HashSet<Type> GetAllTypes(this Type sourceType)
        {
            lock (s_derivedTypesDictionary)
            {
                HashSet<Type> types = null;
                if (!s_derivedTypesDictionary.TryGetValue(sourceType, out types))
                {
                    types = new HashSet<Type>();

                    /// Return all the class types (including this one).
                    for (Type type = sourceType; type != null; type = type.BaseType)
                    {
                        /// Add the underlying enum type before we add System.Enum itself.
                        /// TODO: We may have to recurse to get the underlying type's hierarchy.
                        if (type == typeof(System.Enum))
                        {
                            Type enumUnderlyingType = Enum.GetUnderlyingType(sourceType);
                            types.Add(enumUnderlyingType);
                            types.AddRange(GetAllTypes(enumUnderlyingType));
                        }

                        types.Add(type);
                        if (type.IsGenericType)
                            types.Add(type.GetGenericTypeDefinition());
                    }

                    /// Return all the interfaces.
                    foreach (Type type in sourceType.GetInterfaces())
                    {
                        types.Add(type);
                        if (type.IsGenericType)
                            types.Add(type.GetGenericTypeDefinition());
                    }

                    s_derivedTypesDictionary.Add(sourceType, types);
                }
                return types;
            }
        }

        /**********************************************************************************/
        public static IEnumerable<Type> GetAllTypes<T>(this T source)
        {
            if (source == null)
                return GetAllTypes(typeof(T));
            else
                return GetAllTypes(source.GetType());
        }

        /**********************************************************************************/
        public static Encoding GetEncoding(this ContentType contentType)
        {
            switch (contentType.CharSet.ToLower())
            {
                case "":
                case null:
                {
                    return Encoding.ASCII;
                }
                case "utf-8": return Encoding.UTF8;
                case "utf-16": return Encoding.Unicode;
                case "utf-32": return Encoding.UTF32;
                default: return Encoding.GetEncoding(contentType.CharSet, new EncoderReplacementFallback("?"), new DecoderReplacementFallback("?"));
            }
        }

        /**********************************************************************************/
        /// <summary>
        /// Similar to the SQL IN operator, this concisely performs a one-to-many equality (set) check.
        /// Returns true if the value matches any of the values passed in the parameter list.
        /// </summary>
        public static bool In<T>(this T value, params T[] matchSetArray)
        {
            /// No sense in running this every single iteration.
            Func<T, T, bool> equalityFunc = EqualityComparer<T>.Default.Equals;

            /// for-loop is more efficient than foreach-loop,
            /// and comparisons against zero per iteration are more efficient than comparisons against string.Length.
            for (int iIndex = matchSetArray.Length - 1; iIndex >= 0; iIndex--)
            {
                if (equalityFunc(value, matchSetArray[iIndex]))
                    return true;
            }

            return false;
        }

        /**********************************************************************************/
        /// <summary>
        /// Tests whether the value is within the range, inclusive of the bounds.
        /// Not nearly as efficient as a pure operator comparison, but vastly more readable.
        /// </summary>
        public static bool InRange<T>(this T value, T lower, T upper) where T : IComparable<T>
        {
            /// If (lower > upper) then the condition fails on its own; leave it at that.
            return (value.CompareTo(lower) >= 0) && (value.CompareTo(upper) <= 0);
        }

        /**********************************************************************************/
        public static bool IsNullOrEmpty(this Stream stream)
        {
            return (stream == null) || (stream.Length == 0);
        }

        /**********************************************************************************/
        public static bool IsPipeBroken(this IOException ex)
        {
            Win32ErrorCode errorCode;
            return (TryGetWin32ErrorCode(ex, out errorCode) && errorCode == Win32ErrorCode.ERROR_NO_DATA);
        }

        /**********************************************************************************/
        /// <summary>
        /// Type detection that works on Type objects at runtime instead of type names in the compiler.
        /// </summary>
        public static bool IsType(this object source, Type type)
        {
            return GetAllTypes(source.GetType()).Contains(type);
        }

        /**********************************************************************************/
        /// <summary>
        /// Type detection that works on Type objects at runtime instead of type names in the compiler.
        /// </summary>
        public static bool IsType<TComparison>(this object source)
        {
            return GetAllTypes(source.GetType()).Contains(typeof(TComparison));
        }

        /**********************************************************************************/
        /// <summary>
        /// Type detection that works on Type objects at runtime instead of type names in the compiler.
        /// </summary>
        public static bool IsType(this Type type, Type testType)
        {
            return GetAllTypes(type).Contains(testType);
        }

        /**********************************************************************************/
        /// <summary>
        /// Type detection that works on Type objects at runtime instead of type names in the compiler.
        /// </summary>
        public static bool IsType<TComparison>(this Type type)
        {
            return GetAllTypes(type).Contains(typeof(TComparison));
        }

        /**********************************************************************************/
        /// <summary>
        /// This only supports English for now.
        /// Future versions may look at the current thread culture.
        /// </summary>
        public static string ToLongString(this TimeSpan timeSpan)
        {
            List<string> astrSpanComponentList = new List<string>();
            TimeSpan remainingSpan = timeSpan;

            int iDays = remainingSpan.Days;
            remainingSpan -= TimeSpan.FromDays(iDays);

            if (iDays == 1)
                astrSpanComponentList.Add("1 day");
            else if (iDays > 0)
                astrSpanComponentList.AddFormat("{0} days", iDays);

            int iHours = remainingSpan.Hours;
            remainingSpan -= TimeSpan.FromHours(iHours);

            if (iHours == 1)
                astrSpanComponentList.Add("1 hour");
            else if (iHours > 0)
                astrSpanComponentList.AddFormat("{0} hours", iHours);

            int iMinutes = remainingSpan.Minutes;
            remainingSpan -= TimeSpan.FromMinutes(iMinutes);

            if (iMinutes == 1)
                astrSpanComponentList.Add("1 minute");
            else if (iMinutes > 0)
                astrSpanComponentList.AddFormat("{0} minutes", iMinutes);

            int iSeconds = remainingSpan.Seconds;
            remainingSpan -= TimeSpan.FromSeconds(iSeconds);

            if (iSeconds == 1)
                astrSpanComponentList.Add("1 second");
            else if (iSeconds > 1)
                astrSpanComponentList.AddFormat("{0} seconds", iSeconds);

            /// Down to the last second, if no other components have been specified.
            if (astrSpanComponentList.Count == 0)
                astrSpanComponentList.AddFormat("{0:n00} seconds", (double)remainingSpan.Ticks / (double)TimeSpan.FromSeconds(1).Ticks);

            return astrSpanComponentList.Join(" ");
        }

        /**********************************************************************************/
        /// <summary>
        /// Fluently converts any struct into a Nullable&lt;T&gt;.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <returns></returns>
        public static Nullable<T> ToNullable<T>(this T value) where T : struct
        {
            return (Nullable<T>)value;
        }

        /**********************************************************************************/
        public static double ToTimeUnits(this TimeSpan timeSpan, TimeUnit timeUnit)
        {
            switch (timeUnit)
            {
                case TimeUnit.Days: return timeSpan.TotalDays;
                case TimeUnit.Hours: return timeSpan.TotalHours;
                case TimeUnit.Minutes: return timeSpan.TotalMinutes;
                case TimeUnit.Seconds: return timeSpan.TotalSeconds;
                case TimeUnit.Milliseconds: return timeSpan.TotalMilliseconds;
                case TimeUnit.Microseconds: return timeSpan.TotalMilliseconds * 1000;
                case TimeUnit.NtFileTimeTicks: return timeSpan.TotalMilliseconds * 100000;
                case TimeUnit.Nanoseconds: return timeSpan.TotalMilliseconds * 1000000;
                case TimeUnit.Picoseconds: return timeSpan.TotalMilliseconds * 100000000;
                default: throw new NotSupportedException("The provided unit {0} is either too cultural-subjective or not otherwise supported or implemented.".FormatWith(timeUnit));
            }
        }

        /**********************************************************************************/
        private static PropertyInfo s_Exception_HResult_PropertyInfo = typeof(Exception).GetAllProperties().First(p => p.Name == "HResult");

        public static HRESULT GetHResultByReflection(this Exception ex)
        {
            return (HRESULT)(int)s_Exception_HResult_PropertyInfo.GetValue(ex, null);
        }

        public static bool TryGetWin32ErrorCode(this Exception ex, out Win32ErrorCode errorCode)
        {
            HRESULT hResult = GetHResultByReflection(ex);
            if (hResult.HRESULT_FACILITY() == HResultFacility.FACILITY_WIN32)
            {
                errorCode = (Win32ErrorCode)hResult.HRESULT_CODE();
                return true;
            }

            errorCode = Win32ErrorCode.ERROR_SUCCESS;
            return false;
        }
    }
}